#include <io.h>
#include <console.h>
#include <string.h>

//首先定义一些常量
#define CRT_ADDR_REG 0x3D4 // CRT(6845)索引寄存器
#define CRT_DATA_REG 0x3D5 // CRT(6845)数据寄存器

#define CRT_START_ADDR_H 0xC // 显示内存起始位置 - 高位
#define CRT_START_ADDR_L 0xD // 显示内存起始位置 - 低位
#define CRT_CURSOR_H 0xE     // 光标位置 - 高位
#define CRT_CURSOR_L 0xF     // 光标位置 - 低位
// 内存位置
#define MEM_BASE 0xB8000              // 显卡内存起始位置
#define MEM_SIZE 0x4000               // 显卡内存大小 16k
#define MEM_END (MEM_BASE + MEM_SIZE) // 显卡内存结束位置
#define WIDTH 80                      // 屏幕文本列数
#define HEIGHT 25                     // 屏幕文本行数
#define ROW_SIZE (WIDTH * 2)          // 每行字节数
#define SCR_SIZE (ROW_SIZE * HEIGHT)  // 屏幕字节数

#define ASCII_NUL 0x00
#define ASCII_ENQ 0x05
#define ASCII_ESC 0x1B // ESC
#define ASCII_BEL 0x07 // \a
#define ASCII_BS 0x08  // \b
#define ASCII_HT 0x09  // \t
#define ASCII_LF 0x0A  // \n
#define ASCII_VT 0x0B  // \v
#define ASCII_FF 0x0C  // \f
#define ASCII_CR 0x0D  // \r
#define ASCII_DEL 0x7F

static u32 screenCurr; // 当前显示器当前的内存位置
static u32 pos; // 记录当前光标的内存位置，static定义后表示仅在当前文件可见，不会对外导出暴露
static u32 cursorX, cursorY; // 当前光标的坐标
static u8 attribute = 7; // 字符样式
static u16 space = 0x0720; // 带有样式的空格，字符删除后会用此空格替代，07是样式，20是空格

// 获得硬件中当前显示器的开始位置，与内存位置对应
static u32 GetScreenLoc()
{
    OutByte(CRT_ADDR_REG, CRT_START_ADDR_H); // 设置操作显卡内CRT_START_ADDR_H寄存器，即开始位置高地址
    u32 loc = InByte(CRT_DATA_REG) << 8; // 读取CRT_START_ADDR_H中数据，高8位所以右移8位
    OutByte(CRT_ADDR_REG, CRT_START_ADDR_L); // 设置即将操作CRT_START_ADDR_L寄存器
    loc |= InByte(CRT_DATA_REG); // 读入低8位
    screenCurr = loc << 1; // 位置*2，就是所在内存的相对位置，因为在内存中，两个字节表示一个字符
    screenCurr += MEM_BASE; // 加上基准内存后，获得绝对位置
    return screenCurr;
}

static void setScreenLoc()
{
    OutByte(CRT_ADDR_REG, CRT_START_ADDR_H); // 设置操作显卡内CRT_START_ADDR_H寄存器
    OutByte(CRT_DATA_REG, ((screenCurr - MEM_BASE) >> 9) & 0xff); // 通过数据寄存器把数据写给CRT_START_ADDR_H
    OutByte(CRT_ADDR_REG, CRT_START_ADDR_L); // 设置即将操作CRT_START_ADDR_L寄存器
    OutByte(CRT_DATA_REG, ((screenCurr - MEM_BASE) >> 1) & 0xff); 
}

// 获得硬件中当前光标在屏幕中坐标位置
static void GetCursorPos()
{
    OutByte(CRT_ADDR_REG, CRT_CURSOR_H); // 设置操作显卡内CRT_CURSOR_H寄存器，即开始位置高地址
    pos = InByte(CRT_DATA_REG) << 8; // 读取CRT_CURSOR_H中数据，高8位所以右移8位
    OutByte(CRT_ADDR_REG, CRT_CURSOR_L); // 设置即将操作CRT_CURSOR_L寄存器
    pos |= InByte(CRT_DATA_REG); // 读入低8位
    GetScreenLoc();
    pos <<= 1; // pos *= 2
    pos += MEM_BASE;
    // 光标绝对位置-屏幕起始绝对位置，获得光标相对起始的位置
    u32 delta = (pos - screenCurr) >> 1;
    cursorX = delta % WIDTH;
    cursorY = delta / WIDTH;
}

// 设置光标位置
static void SetCursorPoc()
{
    OutByte(CRT_ADDR_REG, CRT_CURSOR_H); // 设置操作显卡内CRT_START_ADDR_H寄存器
    OutByte(CRT_DATA_REG, ((pos - MEM_BASE) >> 9) & 0xff); // 通过数据寄存器把数据写给CRT_START_ADDR_H
    OutByte(CRT_ADDR_REG, CRT_CURSOR_L); // 设置即将操作CRT_START_ADDR_L寄存器
    OutByte(CRT_DATA_REG, ((pos - MEM_BASE) >> 1) & 0xff); 
}

void ConsoleClear() // 清屏
{
    screenCurr = MEM_BASE;
    pos = MEM_BASE;
    cursorX = 0;
    cursorY = 0;
    SetCursorPoc();
    setScreenLoc();

    u16 *ptr = (u16 *)MEM_BASE;
    while (ptr < (u16 *)MEM_END) {
        *ptr++ = space;
    }
}

static void CommandBs() // 处理ASCII_BS，相当于退格键
{
    if (cursorX) {
        cursorX--; //删除前一个字符
        pos -= 2;
        *(u16 *)pos = space;
    }
}

static void CommandDel() // 处理ASCII_BEL，删除当前字符
{
    *(u16 *)pos = space;
}

static void CommandCr() // 处理ASCII_CR，\r，回到一行开始位置
{
    pos -= (cursorX << 1); // 绝对位置需要减去当前行x的值
    cursorX = 0;
}

static void ScrollUp() // 向上滚动一行
{
    if (screenCurr + SCR_SIZE + ROW_SIZE < MEM_END) { // 加上一个屏幕的大小再加一行的大小不超过内存区域
        u32 *ptr = (u32 *)(screenCurr + SCR_SIZE); // 就将屏幕未显示的第一行清空，设置为空格
        for (size_t i = 0; i < WIDTH; ++i) {
            *ptr = space;
        }
        screenCurr += ROW_SIZE;
        pos += ROW_SIZE;
    } else { // 如果超过了内存缓冲区，则需要将整个屏幕拷贝到内存起始的位置，重新从头开始
        memcpy((u8 *)MEM_BASE, (u8 *)screenCurr, SCR_SIZE); // 也就是把当前屏幕显示的内容整体拷贝到起始位置
        pos -= (screenCurr - MEM_BASE); // 光标位置也移动过去
        screenCurr = MEM_BASE; // 屏幕起始位置也设置回去
    }
    setScreenLoc(); // 每次滚动后，需要重新设置屏幕开始的位置，屏幕的起始位置对应内存缓冲区移动了一行
}

static void CommandLf() // 处理ASCII_LF，\n，换行
{
    if (cursorY + 1 < HEIGHT) {
        cursorY++; // 如果Y坐标没有超过HEIGHT 25行，则直接加
        pos += ROW_SIZE;
    } else { // 如果超过了最大HEIGHT 25行，则显示已经在屏幕最底下那一行了，换行后则需要向上滚动一行
        ScrollUp();
    }
}

void ConsoleWrite(char *buf, u32 count) // 写缓冲区
{
    char ch;
    while (count--) {
        ch = *buf++;
        switch (ch)
        {
        case ASCII_NUL:
            /* code */
            break;
        case ASCII_ENQ:
            break;
        case ASCII_ESC: // ESC
            break;
        case ASCII_BEL: // \a
            break;
        case ASCII_BS:  // \b
            CommandBs();
            break;
        case ASCII_HT:  // \t
            break;
        case ASCII_LF:  // \n
            CommandLf();
            CommandCr();
            break;
        case ASCII_VT:  // \v
            break;
        case ASCII_FF:  // \f
            CommandLf();
            break;
        case ASCII_CR:  // \r
            CommandCr();
            break;
        case ASCII_DEL:
            CommandDel();
            break;
        default:
            if (cursorX > WIDTH) {
                cursorX -= WIDTH;
                pos -= ROW_SIZE;
                CommandLf();
            }
            *(char *)pos = ch;
            pos++;
            *(char *)pos = attribute;
            pos++;
            cursorX++;
            break;
        }
        SetCursorPoc();
    }
}

void ConsoleInit() // 控制器初始化
{
    ConsoleClear();
    GetScreenLoc();
}
